package maumau;

import java.io.*;
import java.util.*;
import java.io.Serializable;

/**
 * @author Mathias Blaha
 * @author Maximilian Reß
 * @version 1.0
 */
public class MauMau_KickAss extends MauMau_Intf implements Serializable {
  
    private Random rand;
    private float epsilon = 0.5f;
    private long numOfPlayedEpisodes;
    static int numOfWonGames;
    private Vector<Integer> stateHash;
    private int action;
    private Vector<Q> episodeQs;
    Vector<Episode> episode;
    HashMap<Vector<Integer>, Vector<Q>> qsa = new HashMap<Vector<Integer>, Vector<Q>>();
    File trainedData = new File("trainedData.obj");
    File fnumOfPlayedEpisodes = new File("numOfPlayedEpisodes.txt");

    /**
     * C'tor
     */
    MauMau_KickAss() {
        
        /**
         * Initialisierung
         */
        
        this.setName("Kick_Ass");
        rand = new Random(0);
        numOfPlayedEpisodes = 0;
        numOfWonGames = 0;
            
        ObjectInputStream readTrainingData = null;
        ObjectOutputStream writeTrainingData = null;
        BufferedReader bReader = null;
        episode = new Vector<Episode>();

        /**
        * Laden der Trainingsdaten
        */
        
        try {
            readTrainingData = new ObjectInputStream(new FileInputStream(trainedData));
            qsa = (HashMap<Vector<Integer>, Vector<Q>>) readTrainingData.readObject();
            readTrainingData.close();
        } catch (Exception e) {
            qsa = new HashMap<Vector<Integer>, Vector<Q>>();
            System.err.println("Error loading " + trainedData + "/n --> creating new HashMap");

        }

        /**
         * Laden der Anzahl bereits gespielter Spiele, bezogen auf die Trainingsdaten
         */
        
        try {
            bReader = new BufferedReader(new FileReader(fnumOfPlayedEpisodes));
            numOfPlayedEpisodes = Long.parseLong(bReader.readLine());
            bReader.close();
        } catch (Exception e) {
            System.err.println("Error loading " + fnumOfPlayedEpisodes);
        }
        
        /**
         * Einleitende Infos
         */
        
        System.out.println("Anzahl Spiele bisher:" + numOfPlayedEpisodes);
        System.out.println("Anzahl Spiele dieser Lauf:" + MauMau.numOfGames);
    }

    ;
        

    public void newGame() {
//        System.out.println(getName() + ": newGame called");
    };
    
/**
* Spielzug / Enthält gesamte Logik   
* @return (int) gewählte Aktion
*/
    
    
	@Override
    public int step(MauMau_State state, Vector<Integer> actions, double reward,
            boolean terminal) {
 
            
/**
* ENDE EINER EPISODE
* terminal == true
* Wichtig da Monte Carlo Algorithmus, hier erfolgt das gesamte Lernen 
*/    
            
        if (terminal) {
            
            numOfPlayedEpisodes++;
                        
            // Zwischenstand ausgeben nach ja 10% der Episoden:

             if ((numOfPlayedEpisodes % (MauMau.numOfGames / 10)) == 0) {
                System.out.println("Zwischenstand: " +  numOfPlayedEpisodes);       
            }
    
            /**
             * positiver Return --> Episode / Spiel gewonnen
             */
             
            if (reward > 0) {
                numOfWonGames++;
            }

            /**
             * Anpassen des Epsilon zum Sicherstellen von Exploration / hardcoded
             */
            
            if (numOfPlayedEpisodes >= 35000000) {
                epsilon = 0.005f;
            } else if (numOfPlayedEpisodes >= 25000) {
                epsilon = 0.01f;
            } else if (numOfPlayedEpisodes >= 20000) {
                epsilon = 0.1f;
            } else if (numOfPlayedEpisodes >= 15000) {
                epsilon = 0.2f;
            } else if (numOfPlayedEpisodes >= 10000) {
                epsilon = 0.3f;
            }

            /**
             * Update auf alle Q-Werte (episodeQs) in der Q(s,a), welche in der letzten Episode (episode) besucht wurden
             */
            
            for (int i = 0; i < episode.size(); i++) {
                
                episodeQs = qsa.get(episode.get(i).episodeState);
//                System.out.println("Qs der Episode: " + episodeQs);

                for (int j = 0; j < episodeQs.size(); j++) {

                    if (episodeQs.elementAt(j).action == episode.get(i).episodeAction) {
                        setReward(episodeQs.elementAt(j), reward);
                    }
                }
            }
            
            /**
             * Reset des Logs der States der vergangenen Episode  
             */
            
            episode.clear();
            return 0;
        }

 
/**
* ENDE EINES SPIELZUGS
* Wählen einer geeigneten / gelernten Aktion
* oder Erzeugne eines neuen States
*/
        
        
        /**
         * Generieren eines eindeutigen Schlüssels aus dem aktuellen State-Objekt, dieser ist die Implementierung des "state"
         */
        
        stateHash = preHash(state.getOpenCard(), state.getOpenCardMult(), actions);       
        
        /**
         * State ist bereits bekannt --> Wählen einer Aktion
         */
                
        if (qsa.containsKey(stateHash)) {

            Vector<Q> q = qsa.get(stateHash);

            if (rand.nextFloat() < epsilon) {
                action = performRandomAction(actions);
            } else {

                int zuletztGewaehlt = 0;
                int greedyAction = q.elementAt(0).action;
                double tmpReward = q.elementAt(0).averageReward;


                for (int i = 1; i < q.size(); i++) {


                    if (q.elementAt(i).averageReward > tmpReward) {
                        greedyAction = q.elementAt(i).action;
                        tmpReward = q.elementAt(i).averageReward;
                        zuletztGewaehlt = i;
                    } else if (q.elementAt(i).averageReward == tmpReward) {

                        if (rand.nextFloat() >= 0.5f) {
                            greedyAction = q.elementAt(zuletztGewaehlt).action;
                            tmpReward = q.elementAt(zuletztGewaehlt).averageReward;
                            zuletztGewaehlt = i;
                        } else {
                            greedyAction = q.elementAt(i).action;
                            tmpReward = q.elementAt(i).averageReward;
                            zuletztGewaehlt = i;
                        }
                    }
                }
                action = greedyAction;
            }

            
        /**
         * State ist noch unbekannt
         * --> Anlegen des neuen States
         * --> zufällige Wahl einer möglichen Aktion
         */
            
        } else {
           
            /**
             * Erzeugen eines neuen States
             */
            
            Vector<Q> q = new Vector<Q>();  
            for (int i = 0; i < actions.size(); i++) {
                q.add(new Q(actions.get(i)));        
            }        
            for (int i = 0; i < stateHash.size(); i++)
            qsa.put(stateHash, q);
            
            action = performRandomAction(actions);
            // Alle Q(s,a) anlegen      
            
        }
        
        /**
         * Loggen der in diesem Zug gewählten (s,a)-Kombination
         */
        
        episode.add(new Episode(stateHash, action));
        return action;
    }

    /**
     * Wählt eine beliebige Aktion aus den in diesem Zug möglichen Aktionen (actions) 
     * @param actions
     */
        
    private int performRandomAction(Vector<Integer> actions) {
        int pos = rand.nextInt(actions.size());
        Integer i = actions.get(pos);
        return i.intValue();
    }
    
    /**
     * Inkrementelles Update auf die Qs
     * @param q
     * @param gainedReward 
     */

    private void setReward(Q q, double gainedReward) {
        q.averageReward = q.averageReward + (gainedReward - q.averageReward) / (q.k + 1);
        q.k++;
    }

//        private void setReward(Q q, double gainedReward) {
//        q.averageReward = (q.averageReward * q.k + gainedReward)
//                / (q.k + 1);
//        q.k++;
//    }
    
    
    /**
     * Generieren eines eindeutigen Schlüssels, dieser ist die Implementierung des "state" und dient als state-key für die HashMap
     * Änderungen des state durch hinzufügen weiterer Parameter
     * @param openCard offen liegende Karte
     * @param openCardMult anzahl der 7er   
     * @param actions mögliche Aktionen
     * @return 
     */
     
    private Vector<Integer> preHash (Integer openCard, Integer openCardMult ,Vector<Integer> actions) {       
        Vector<Integer> prehash = new Vector<Integer>();
        prehash.add(openCard);
        prehash.add(openCardMult);
        Collections.sort(actions);
        for (int i = 0; i < actions.size(); i++) {
            prehash.add(actions.get(i));                      
        }       
            return prehash;
    }
    
   /**
    * Datentyp zum sequentiellen Aufzeichnen der (state,action) Kombinationen einer Episode
    */ 
   
    private class Episode {

        Vector<Integer> episodeState;
        int episodeAction;

        public Episode(Vector<Integer> stateHash, int action) {
            this.episodeState = stateHash;
            this.episodeAction = action;
        }
    }

    /**
     * Datentyp für a,Q Paare
     * bilden die Values in der HashMap qsa, zum key stateHash (einem State)
     */
    
    private class Q  implements Serializable {

        int k;                  // Anzahl der Updates auf Q und wie oft Aktion a gespielt wurde
        double averageReward;
        int action;

        Q(int action) {
            k = 0;
            this.averageReward = 0.0;
            this.action = action;
        }

        @Override
        public String toString() {            
            String out = "| Action: " + this.action + "| Reward: " + this.averageReward + "| k: " + k;
            return out;
        }   
    }
        
    public void cleanup() {
        
        System.out.println(getName() + ": Cleanup called");
        
        /**
         * Persistenz der Trainingsdaten
         * 
         * Files im Workspace:
         * trainedData.obj
         * numOfPlayedEpisodes.txt
         */        
        
        ObjectOutputStream serializedHashMap = null;
        BufferedWriter bWriter = null;

        try {
            bWriter = new BufferedWriter(new FileWriter(fnumOfPlayedEpisodes));
            bWriter.write(Long.valueOf(numOfPlayedEpisodes).toString());
            serializedHashMap = new ObjectOutputStream(new FileOutputStream(trainedData));
            serializedHashMap.writeObject(qsa);
            serializedHashMap.flush();
            serializedHashMap.close();
            bWriter.flush();
            bWriter.close();
        } catch (Exception e) {
            System.out.println(e);
            System.err.println("error writing trainedData");
        }

        
        /**
         * Auswertung nach Ende aller Spiele
         */
        
        System.out.println("######################################");
        System.out.println("Anzahl Spiele gesamt: " + numOfPlayedEpisodes);
        System.out.println("Anzahl Spiele in diesem Lauf: " + MauMau.numOfGames);
        System.out.println("Anzahl gewonnener Spiele (): " + numOfWonGames);
        System.out.println("Gewonnen in %: " + (float) numOfWonGames / (float) MauMau.numOfGames * 100.0);               
        System.out.println("Größe der qsa: " + qsa.size());
        System.out.println("######################################");
        
        /**
         * Inhalte der qsa map
         */
        
//        for (Object values : qsa.values()){
//            System.out.println(values);
//        }
        
//        for (Vector<Integer> key : qsa.keySet()){
//            System.out.println(key);
//        }
 
    }
}